using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.Collections;
using System.Reflection;
using System.ComponentModel;
using System.Runtime.InteropServices;
using GoogleMaps.Configuration;
using System.Globalization;

namespace GoogleMaps
{
	/// <summary>
	/// The focal point of the Google Maps Control API. This WebControl is used to configure a new instance
	/// of a Google Map on an ASP.NET web page.
	/// </summary>
	[GuidAttribute("30110F36-2569-41cc-9732-A064D34165FD"),
	ToolboxData("<{0}:GMap runat=\"server\"></{0}:GMap>")]
	public class GMap : WebControl
	{
		private const String DefaultContainerId = "map";
		private const String DefaultContainerTag = "div";
		private const String JavaScriptVariableNameRoot = "gmap";
		private const String GoogleMapsJavaScriptHref = "http://maps.google.com/maps?file=api&v={0}&key={1}";
		private Boolean outputLocalSearchStyles;

		/// <summary>
		/// Initializes a new instance of the GMap class.
		/// </summary>
		public GMap()
			: this(null, null)
		{
		}

		/// <summary>
		/// Initializes a new instance of the Gmap class.
		/// </summary>
		/// <param name="containerId">The id of the container that the gmap should be assigned to.</param>
		public GMap(String containerId)
			: this(containerId, null)
		{
		}

		/// <summary>
		/// Initializes a new instance of the Gmap class with the parameters provided
		/// </summary>
		/// <param name="containerId">The id of the container that the gmap should be assigned to.</param>
		/// <param name="options">The options that are supposed to be added the the GMap when it is constructed.</param>
		public GMap(String containerId, GMapOptions options)
		{
			GMarkers = new GMarkerCollection();
			GIcons = new GIconCollection();
			GLatLngs = new GLatLngCollection();
			GControls = new GControlCollection();
			GEvents = new GEventCollection();
			GPolylines = new GPolylineCollection();
			GPolygons = new GPolygonCollection();

			ContainerId = containerId;
			Options = options;
			EnableDragging = true;
			PanningDuration = 1000;
			EnableInfoWindow = true;
			ApiVersion = "2.x";
		}

		/// <summary>
		/// Handles the Load event from the Control class.
		/// </summary>
		protected override void OnLoad(EventArgs e)
		{
			base.OnLoad(e);
		}

		/// <summary>
		/// Handels the PreRender event from the Control class.
		/// </summary>
		/// <param name="e"></param>
		protected override void OnPreRender(EventArgs e)
		{
			base.OnPreRender(e);

			if (String.IsNullOrEmpty(JavaScriptVariableName))
				JavaScriptVariableName = String.Format(CultureInfo.InvariantCulture, "{0}_{1}", JavaScriptVariableNameRoot, ClientID);

			Page.ClientScript.RegisterClientScriptResource(typeof(GoogleMaps.GMap), "GoogleMaps.HelperFunctions.js");
			Page.ClientScript.RegisterClientScriptInclude(typeof(GoogleMaps.GMap), "GoogleMapsApi", String.Format(CultureInfo.InvariantCulture, GoogleMapsJavaScriptHref, ApiVersion, ApiKey));

			if (GControls != null)
			{
				foreach (GControl control in GControls)
				{
					if (control.GetType() == typeof(LocalSearchControl))
					{
						if (control.UseDefaultGoogleStyles)
							outputLocalSearchStyles = true;

						Page.ClientScript.RegisterClientScriptInclude(typeof(GoogleMaps.GMap), "GoogleApi", "http://www.google.com/uds/api?file=uds.js&amp;v=1.0");
						Page.ClientScript.RegisterClientScriptInclude(typeof(GoogleMaps.GMap), "GoogleMapsLocalSearch", "http://www.google.com/uds/solutions/localsearch/gmlocalsearch.js");
						break;
					}
				}
			}
		}

		/// <summary>
		/// Renders the control as JavaScript to the specified Html Writer (Overrides WebControl.Render(HtmlTextWriter).)
		/// </summary>
		/// <param name="writer">The HtmlTextWriter object that receives the control content.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
		protected override void Render(HtmlTextWriter writer)
		{
			if (String.IsNullOrEmpty(ApiKey))
				throw new InvalidOperationException(Properties.Resources.ApiKeyNullOrEmpty);

			if (!HasContainer)
			{
				writer.AddAttribute(HtmlTextWriterAttribute.Id, DefaultContainerId);
				writer.AddStyleAttribute(HtmlTextWriterStyle.Width, Width.ToString());
				writer.AddStyleAttribute(HtmlTextWriterStyle.Height, Height.ToString());
				writer.RenderBeginTag(DefaultContainerTag);
				writer.RenderEndTag();
			}

			if (outputLocalSearchStyles)
			{
				writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/css");
				writer.RenderBeginTag(HtmlTextWriterTag.Style);
				writer.Write("@import url(\"http://www.google.com/uds/css/gsearch.css\");");
				writer.Write("@import url(\"http://www.google.com/uds/solutions/localsearch/gmlocalsearch.css\");");
				writer.RenderEndTag();
			}

			StringBuilder js = new StringBuilder();
			js.Append("if (GBrowserIsCompatible()) {");

			foreach (GIcon gIcon in GIcons)
				gIcon.Render(js);

			if (EnablePanning && GLatLngs.Count > 1)
			{
				StringBuilder pp = new StringBuilder();
				foreach (GLatLng latLng in GLatLngs)
				{
					if (pp.Length > 0)
						pp.Append(",");

					pp.AppendFormat(CultureInfo.InvariantCulture, "new GLatLng({0}, {1})", latLng.Lat.ToString(CultureInfo.InvariantCulture), latLng.Lng.ToString(CultureInfo.InvariantCulture));
				}

				js.AppendFormat(CultureInfo.InvariantCulture, "var {0}_currentPanPoint = 0;", JavaScriptVariableName);
				js.AppendFormat(CultureInfo.InvariantCulture, "var {0}_panPoints = new Array({1});", JavaScriptVariableName, pp.ToString());
			}

			js.AppendFormat(CultureInfo.InvariantCulture, "var {0} = new GMap2(document.getElementById(\"{1}\"));", JavaScriptVariableName, GetContainerId());

			if (!EnableDragging)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.disableDragging();", JavaScriptVariableName);

			if (!EnableInfoWindow)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.disableInfoWindow();", JavaScriptVariableName);

			if (EnableDoubleClickZoom)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.enableDoubleClickZoom();", JavaScriptVariableName);

			if (EnableContinuousZoom)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.enableContinuousZoom();", JavaScriptVariableName);

			if (EnableGoogleBar)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.enableGoogleBar();", JavaScriptVariableName);

			if (EnableScrollWheelZoom)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.enableScrollWheelZoom();", JavaScriptVariableName);

			if (MapCenter != null)
			{
				if (MapCenter.ZoomLevel > 0)
					js.AppendFormat(CultureInfo.InvariantCulture, "{0}.setCenter(new GLatLng({1}, {2}), {3});", JavaScriptVariableName, MapCenter.Lat.ToString(CultureInfo.InvariantCulture), MapCenter.Lng.ToString(CultureInfo.InvariantCulture), MapCenter.ZoomLevel.ToString(CultureInfo.InvariantCulture));
				else
					js.AppendFormat(CultureInfo.InvariantCulture, "{0}.setCenter(new GLatLng({1}, {2}));", JavaScriptVariableName, MapCenter.Lat.ToString(CultureInfo.InvariantCulture), MapCenter.Lng.ToString(CultureInfo.InvariantCulture));
			}
			else if (GMarkers.Count == 0)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.setCenter(new GLatLng(37.0625, -95.677068000000006), 3);", JavaScriptVariableName);

			if (isZoomLevelSet)
				js.AppendFormat(CultureInfo.InvariantCulture, "{0}.setZoom({1});", JavaScriptVariableName, ZoomLevel.ToString(CultureInfo.InvariantCulture));

			foreach (GMarker marker in GMarkers)
				marker.Render(js, JavaScriptVariableName);

#if TRIAL
			js.Append("alert('Trial Version of Google Maps Control from www.googlemapscontrol.com');");
			js.AppendFormat("var __tb = {0}.getBounds();", JavaScriptVariableName);
			js.Append("var __tsw = __tb.getSouthWest();");
			js.Append("var __tne = __tb.getNorthEast();");
			js.Append("var __lns = __tne.lng() - __tsw.lng();");
			js.Append("var __las = __tne.lat() - __tsw.lat();");
			js.AppendFormat("var __{0}m = new GMarker(new GLatLng(__tsw.lat() + __las * Math.random(), __tsw.lng() + __lns * Math.random()));", JavaScriptVariableName);
			js.AppendFormat("GEvent.addListener(__{0}m, \"click\", function() {{__{0}m.openInfoWindowHtml(\"<a href='http://www.googlemapscontrol.com\'>www.googlemapscontrol.net</a>\");}});", JavaScriptVariableName);
			js.AppendFormat("{0}.addOverlay(__{0}m);", JavaScriptVariableName);
#endif

			foreach (GControl control in GControls)
				control.Render(js, JavaScriptVariableName);

			foreach (GEvent ge in GEvents)
				ge.Render(js, JavaScriptVariableName);

			foreach (GPolyline pl in GPolylines)
				pl.Render(js, JavaScriptVariableName);

			foreach (GPolygon pg in GPolygons)
				pg.Render(js, JavaScriptVariableName);

			if (EnablePanning && GLatLngs.Count == 1)
				js.AppendFormat(CultureInfo.InvariantCulture, "window.setTimeout(function(){{{0}.panTo(new GLatLng({1}, {2}));}}, {3});", JavaScriptVariableName, GLatLngs[0].Lat.ToString(CultureInfo.InvariantCulture), GLatLngs[0].Lng.ToString(CultureInfo.InvariantCulture), PanningDuration.ToString(CultureInfo.InvariantCulture));
			else if (EnablePanning && GLatLngs.Count > 1)
				js.AppendFormat(CultureInfo.InvariantCulture, "window.setTimeout(function(){{panPoints({0}, {0}_panPoints, {0}_currentPanPoint, {1}, {2});}}, {1});", JavaScriptVariableName, PanningDuration.ToString(CultureInfo.InvariantCulture), LoopPanning.ToString().ToLower(CultureInfo.InvariantCulture));

			js.Append("}");

			Page.ClientScript.RegisterStartupScript(this.GetType(), GetContainerId(), js.ToString(), true);
		}

		private String GetContainerId()
		{
			if (!HasContainer)
				return DefaultContainerId;

			return ContainerId;
		}

		/// <summary>
		/// Gets or sets the center point for the map.
		/// </summary>
		[PersistenceMode(PersistenceMode.InnerProperty)]
		public MapCenter MapCenter { get; set; }

		/// <summary>
		/// Gets a collection of markers associated with the map.
		/// </summary>
		[PersistenceMode(PersistenceMode.InnerProperty)]
		public GMarkerCollection GMarkers { get; private set; }

		/// <summary>
		/// Gets a collection of icons associated with the map.
		/// </summary>
		[PersistenceMode(PersistenceMode.InnerProperty)]
		public GIconCollection GIcons { get; private set; }

		/// <summary>
		/// Gets a collection of coordinates associated with the map.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lngs"),
		PersistenceMode(PersistenceMode.InnerProperty)]
		public GLatLngCollection GLatLngs { get; private set; }

		/// <summary>
		/// Gets a collection of controls associated with the map.
		/// </summary>
		[PersistenceMode(PersistenceMode.InnerProperty)]
		public GControlCollection GControls { get; private set; }

		/// <summary>
		/// Gets a collections of events that are wired to the map.
		/// </summary>
		[PersistenceMode(PersistenceMode.InnerProperty)]
		public GEventCollection GEvents { get; private set; }

		/// <summary>
		/// Gets a collection of polylines that are drawn on the map.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Polylines"),
		PersistenceMode(PersistenceMode.InnerProperty)]
		public GPolylineCollection GPolylines { get; private set; }

		/// <summary>
		/// Gets a collection polygons that are drawn on the map.
		/// </summary>
		[PersistenceMode(PersistenceMode.InnerProperty)]
		public GPolygonCollection GPolygons { get; private set; }

		private string apiKey;
		/// <summary>
		/// Gets or sets the api key that is associated to the domain and required by Google
		/// before it will generate the map for the end user.
		/// </summary>
		public String ApiKey
		{
			get
			{
				// if the api key has been set directly then give it preference.
				if (!String.IsNullOrEmpty(apiKey))
					return apiKey;

				// we got here because a key hasn't been specified so get the config section
				GoogleMapsSection config = GoogleMapsSection.Section;

				if (config != null)
					return config.ApiKey;

				// we don't have an api key so just return null
				return null;
			}
			set { apiKey = value; }
		}

		private String containerId;
		/// <summary>
		/// The id of the container that will hold the map. This is ussualy something like a div tag.
		/// </summary>
		public String ContainerId
		{
			get { return containerId; }
			set
			{
				if (containerId != value)
				{
					containerId = value;
					HasContainer = true;
				}
			}
		}

		/// <summary>
		/// Gets or sets a flag indicating that this map has a specified container.
		/// </summary>
		public Boolean HasContainer { get; set; }

		/// <summary>
		/// Gets or sets the name of the javascript variable that the map should be created with.
		/// </summary>
		public String JavaScriptVariableName { get; set; }

		/// <summary>
		/// Gets or sets a flag indicated that dragging is enabled.
		/// </summary>
		public Boolean EnableDragging { get; set; }

		/// <summary>
		/// Gets or sets a flag indicated that the info window is enabled.
		/// </summary>
		public Boolean EnableInfoWindow { get; set; }

		/// <summary>
		/// Gets or sets a flag indicating that double clicking the map will cause it to zoom in.
		/// </summary>
		public Boolean EnableDoubleClickZoom { get; set; }

		/// <summary>
		/// Gets or sets a flag indicating that the a continous smooth zooming effect will be used when
		/// the browser supports it.
		/// </summary>
		public Boolean EnableContinuousZoom { get; set; }

		/// <summary>
		/// Gets or sets a flag indicating that the use of the scroll wheel will zoome the map in and out.
		/// </summary>
		public Boolean EnableScrollWheelZoom { get; set; }

		/// <summary>
		/// Gets or sets a flag indicating that an integrated Google search bar should apear on the map.
		/// </summary>
		public Boolean EnableGoogleBar { get; set; }

		/// <summary>
		/// Gets or sets the collection of options that are associated with the map.
		/// </summary>
		public GMapOptions Options { get; set; }

		/// <summary>
		/// Gets or sets a flag indicating that the map should be able to pan across the collection
		/// of GLatLngs associated with the map.
		/// </summary>
		public Boolean EnablePanning { get; set; }

		/// <summary>
		/// Gets or sets the number of milliseconds it will take to move between to points when panning.
		/// </summary>
		public Int32 PanningDuration { get; set; }

		/// <summary>
		/// Gets or sets a flag indicating that the panning should operate in a continuous loop.
		/// </summary>
		public Boolean LoopPanning { get; set; }

		/// <summary>
		/// Gets or sets a the datasource for the markers that should be placed on the map.
		/// </summary>
		public Object MarkersDataSource { get; set; }

		/// <summary>
		/// Gets or sets the name of the property in the databound object that holds the latitude value.
		/// </summary>
		/// <remarks>It is not necessary to provide this information if the object implements GoogleMaps.IGMarker</remarks>
		public String MarkerLatMember { get; set; }

		/// <summary>
		/// Gets or sets the name of the property in the databound object that holds the longitude value.
		/// </summary>
		/// <remarks>It is not necessary to provide this information if the object implements GoogleMaps.IGMarker</remarks>
		public String MarkerLngMember { get; set; }

		/// <summary>
		/// Gets or sets the version of the Google Maps API that should be used.
		/// </summary>
		public string ApiVersion { get; set; }

		private Byte zoomLevel;
		private Boolean isZoomLevelSet;
		/// <summary>
		/// Gets or sets the zoom level that should be used when rendering the map.
		/// </summary>
		public Byte ZoomLevel
		{
			get { return zoomLevel; }
			set
			{
				if (value > 19 || value < 0)
					throw new ArgumentOutOfRangeException(Properties.Resources.ZoomLevelOutOfRange);

				zoomLevel = value;
				isZoomLevelSet = true;
			}
		}

		/// <summary>
		/// Binds a datasource to this server control so that the rendering of the control will produce
		/// all of the JavaScript necessary for the data to be represented on the map.
		/// </summary>
		public override void DataBind()
		{
			if (MarkersDataSource != null)
			{
				if (GMarkers == null)
					GMarkers = new GMarkerCollection();

				if (MarkersDataSource is DataTable)
				{
					if (String.IsNullOrEmpty(MarkerLatMember) || String.IsNullOrEmpty(MarkerLngMember))
						throw new InvalidOperationException(Properties.Resources.MarkerLatLngMembersNull);

					DataTable dt = (DataTable)MarkersDataSource;

					foreach (DataRow r in dt.Rows)
					{
						GMarkers.Add(new GMarker()
						{
							Lat = Convert.ToDecimal(r[MarkerLatMember], CultureInfo.InvariantCulture),
							Lng = Convert.ToDecimal(r[MarkerLngMember], CultureInfo.InvariantCulture)
						});
					}
					return;
				}
				else if (MarkersDataSource is IEnumerable)
				{
					// first test to see if the guts of what we are looping over are IGMarker
					foreach (Object obj in (IEnumerable)MarkersDataSource)
					{
						IGMarker igm = obj as IGMarker;
						if (igm != null)
						{
							GMarkers.Add(new GMarker()
							{
								Lat = igm.Lat,
								Lng = igm.Lng
							});
						}
						else
						{
							if (String.IsNullOrEmpty(MarkerLatMember) || String.IsNullOrEmpty(MarkerLngMember))
								throw new InvalidOperationException(Properties.Resources.MarkerLatLngMembersNull);

							Type t = obj.GetType();
							PropertyInfo latInfo = t.GetProperty(MarkerLatMember);
							PropertyInfo lngInfo = t.GetProperty(MarkerLngMember);

							GMarkers.Add(new GMarker()
							{
								Lat = Convert.ToDecimal(latInfo.GetValue(obj, null), CultureInfo.InvariantCulture),
								Lng = Convert.ToDecimal(lngInfo.GetValue(obj, null), CultureInfo.InvariantCulture)
							});
						}
					}

					return;
				}

				throw new InvalidOperationException(Properties.Resources.MarkersDataSourceNotValidType);
			}
		}
	}
}
